iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
生成式 AI

AI LeetCode 助教:30 天打造智慧刷題系統系列 第 4

Day 4:提示系統功能整合

  • 分享至 

  • xImage
  •  

今日目標

在完成使用者註冊、題庫建置,以及程度測驗的功能後,將要進一步優化作答體驗,加入提示系統。這個功能的核心概念是,當使用者遇到困難時,可以點選「提示」,獲得一些解題方向或思路,但同時為了避免濫用,若使用提示,系統會在計分時進行扣分處理。

實作步驟

1. 後端:為 Problem 新增hint欄位(model.py)

在現有 Problem 模型加入 hint,以儲存每題的提示。

# backend/models.py
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base

class Problem(Base):
    __tablename__ = "problems"
    id = Column(Integer, primary_key=True, index=True)
    slug = Column(String, unique=True, index=True)
    title = Column(String, index=True)
    difficulty = Column(String)    
    topic = Column(String)     
    is_active = Column(Boolean, default=True)
    hint = Column(String, nullable=True)  # 新增:提示文字,可為 None

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    name = Column(String)
    level = Column(String, default="beginner")
    is_active = Column(Boolean, default=True)
    submissions = relationship("Submission", back_populates="user")

class Submission(Base):
    __tablename__ = "submissions"
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"))
    problem_id = Column(Integer, ForeignKey("problems.id"))
    language = Column(String, default="python")
    code = Column(Text)
    verdict = Column(String, default="pending")  # pending / accepted / wrong / tle ...
    created_at = Column(DateTime, default=datetime.utcnow)
    user = relationship("User", back_populates="submissions")

2. 後端:讓API讀/寫 hint(dto.py)

讓題目輸入輸出(ProblemCreate、ProblemOut)與抽題輸出(AssessmentProblem)都包含 hint。
在原本的 backend/dto.py 基礎上,為下列類別加入 hint 欄位(其它類別不變)。

# backend/dto.py
from pydantic import BaseModel, ConfigDict
from typing import Optional, Literal, List

class ProblemOut(BaseModel):
    id: int
    slug: str
    title: str
    difficulty: str
    topic: str
    hint: Optional[str] = None           # 回傳也帶 hint
    model_config = ConfigDict(from_attributes=True)

class ProblemCreate(BaseModel):
    slug: str
    title: str
    difficulty: Literal["Easy", "Medium", "Hard"]
    topic: str
    hint: Optional[str] = None           # 建立時可帶 hint

class AssessmentProblem(BaseModel):
    id: int
    slug: str
    title: str
    difficulty: str
    topic: str
    hint: Optional[str] = None    # 抽題回應帶 hint
    model_config = ConfigDict(from_attributes=True)

3. 後端:POST 接受 hint(routers/problems.py)

讓新增題目時可以一併傳 hint

from fastapi import APIRouter, Depends, Query, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from ..database import SessionLocal
from .. import models
from ..dto import ProblemOut, ProblemCreate

router = APIRouter(prefix="/problems", tags=["problems"])

def get_db():
    db = SessionLocal()
    try: yield db
    finally: db.close()

@router.get("", response_model=List[ProblemOut])
def list_problems(difficulty: Optional[str] = Query(None),
                  topic: Optional[str] = Query(None),
                  q: Optional[str] = Query(None),
                  db: Session = Depends(get_db)):
    qs = db.query(models.Problem).filter(models.Problem.is_active == True)
    if difficulty: qs = qs.filter(models.Problem.difficulty == difficulty)
    if topic: qs = qs.filter(models.Problem.topic == topic)
    if q: qs = qs.filter(models.Problem.title.ilike(f"%{q}%"))
    return qs.order_by(models.Problem.id.asc()).all()

@router.post("", response_model=ProblemOut, status_code=status.HTTP_201_CREATED)
def create_problem(payload: ProblemCreate, db: Session = Depends(get_db)):
    # 確保 slug 唯一
    exists = db.query(models.Problem).filter(models.Problem.slug == payload.slug).first()
    if exists: raise HTTPException(409, "slug already exists")
    # payload.model_dump() 會包含 hint(可為 None)
    obj = models.Problem(**payload.model_dump(), is_active=True)
    db.add(obj); db.commit(); db.refresh(obj)
    return obj

4. 後端:範例題目附上 hint(seed_mock.py)

讓種子資料直接帶提示,方便前端測試按鈕與扣分。

MOCK = [
    {"slug":"two-sum","title":"Two Sum","difficulty":"Easy","topic":"Array",
     "hint":"用哈希表 O(n) 找 complement"},
    {"slug":"longest-substring","title":"Longest Substring Without Repeating Characters",
     "difficulty":"Medium","topic":"HashMap",
     "hint":"滑動視窗 + 記錄最後出現位置"},
    {"slug":"median-of-two-sorted-arrays","title":"Median of Two Sorted Arrays",
     "difficulty":"Hard","topic":"Binary Search",
     "hint":"對短陣列做二分,維持左右分割平衡"}
]

5. 前端:顯示提示按鈕+扣分旗標(frontend/app.py)

在 Day 3 的測驗流程上,加入「顯示提示」按鈕。按了之後:

  • 畫面顯示 hint 內容
  • 自動把該題的 hint_used=True 記錄到送分 payload 中
import streamlit as st, requests, os
API_BASE = os.getenv("API_BASE", "http://127.0.0.1:8000")

st.header("程度測驗(3 題快速分級)- 含提示")

# 狀態初始化:抽題結果、各題是否看過提示、各題回報
if "assessment_user_id" not in st.session_state:
    st.session_state.assessment_user_id = 1
if "assessment_problems" not in st.session_state:
    st.session_state.assessment_problems = None
if "assessment_results" not in st.session_state:
    st.session_state.assessment_results = {}
if "hint_shown" not in st.session_state:
    st.session_state.hint_shown = {}  # {problem_id: bool}

# 抽題
if st.button("開始測驗 / 重新抽題"):
    try:
        resp = requests.get(f"{API_BASE}/assessment/start",
                            params={"user_id": st.session_state.assessment_user_id}, timeout=8)
        problems = resp.json()["problems"]
        st.session_state.assessment_problems = problems
        st.session_state.assessment_results = {}
        st.session_state.hint_shown = {p["id"]: False for p in problems}
        st.success("已抽出題目,請回報結果或查看提示")
    except Exception as e:
        st.error(f"抽題失敗:{e}")

# 顯示題目 + 提示按鈕 + 回報結果
if st.session_state.assessment_problems:
    st.write("請到 LeetCode 作答,或根據理解回報本題結果:")
    for p in st.session_state.assessment_problems:
        pid = p["id"]
        st.markdown(f"- **[{p['difficulty']}] {p['title']}** / topic: {p['topic']} / slug: `{p['slug']}`")

        # 有 hint 才顯示按鈕;按下就顯示並記錄使用提示
        if p.get("hint"):
            if not st.session_state.hint_shown.get(pid, False):
                if st.button(f"顯示提示(題目 #{pid})", key=f"hintbtn_{pid}"):
                    st.session_state.hint_shown[pid] = True
                    st.info(p["hint"])
            else:
                st.info(p["hint"])

        verdict = st.selectbox(
            f"結果(題目 #{pid})", ["accepted", "wrong", "tle", "skipped"], key=f"verdict_{pid}"
        )
        hint_used = st.session_state.hint_shown.get(pid, False)  # ← 關鍵:是否按過提示
        st.session_state.assessment_results[pid] = {
            "problem_id": pid, "verdict": verdict, "hint_used": hint_used
        }

    # 送出評分
    if st.button("送出並計分"):
        try:
            payload = {
                "user_id": st.session_state.assessment_user_id,
                "items": list(st.session_state.assessment_results.values())
            }
            resp = requests.post(f"{API_BASE}/assessment/score", json=payload, timeout=10)
            data = resp.json()
            if resp.status_code >= 400:
                st.error(f"計分失敗:{data}")
            else:
                st.success(f"建議等級:**{data['level']}**(score={data['score']})")
                with st.expander("查看每題計分"):
                    for pid, info in data["breakdown"].items():
                        st.write(f"- #{pid} {info['title']} / {info['difficulty']} → "
                                 f"{info['verdict']} / hint={info['hint_used']} / score={info['score']}")
        except Exception as e:
            st.error(f"計分失敗:{e}")

做完以上步驟後,執行
後端

python -m uvicorn backend.app:app --reload

前端

cd frontend
python -m streamlit run app.py

按「開始測驗 / 重新抽題」→ 有 hint 的題會看到按鈕。
按下提示 → 畫面顯示提示內容;送分後 breakdown 中該題 hint_used: true,分數-0.2。

今日成果

今天已經成功完成提示系統的基本功能,包含前端顯示提示按鈕、後端儲存與回傳提示文字,以及評分時自動扣分的邏輯。使用者在作答過程中,如果遇到瓶頸,可以選擇點擊提示按鈕,系統就會顯示預先設定的思路,協助他們找到解題方向。此功能能幫助卡關的使用者順利推進,但同時若使用提示,也會在分數計算上有相應的扣分,避免濫用功能,未來計畫加入自動提示生成,讓系統能根據題目類型自動產出思路,進一步提升互動性與智慧化程度。
https://ithelp.ithome.com.tw/upload/images/20250818/20146177H6UcnxgbqP.png
https://ithelp.ithome.com.tw/upload/images/20250818/20146177Q1OHijuO4p.png


上一篇
Day 3:新增程度測驗流程
系列文
AI LeetCode 助教:30 天打造智慧刷題系統4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言